/*
 * GContestUtils.java
 * Created on 26 août 07
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2007, Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.data.helper;

import java.math.BigDecimal;
import java.util.List;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.contest.GContest;
import com.hockeo.vhbase.data.dao.contest.GContestRound;
import com.hockeo.vhbase.data.dao.contest.GKO3ContestElement;
import com.hockeo.vhbase.data.dao.contest.GLeague;
import com.hockeo.vhbase.data.dao.contest.GRoundRobinContestElement;
import com.hockeo.vhbase.data.dao.standings.GStandings;

/**
 * Contains static utility methods for dealing with contests.
 *
 * @version $Id$
 * @author jjanke
 */
public final class GContestUtils
{
  /** Private constructor to avoid instantiation. */
  private GContestUtils()
  {}

  /**
   * Calculates the number of points based on the given number of wins, losses etc.
   *
   * @param nNumWins the number of wins
   * @param nNumWinsOT the number of wins after shootout
   * @param nNumLossesOT the number of losses after shootout
   * @param nNumLosses the number of losses
   * @return the total number of points corresponding to the given results
   */
  public static int getNumPoints( int nNumWins, int nNumWinsOT, int nNumLossesOT, int nNumLosses )
  {
    int nPoints = 0;

    nPoints += GMatchResult.VICTORY.getNumPoints() * nNumWins;
    nPoints += GMatchResult.VICTORY_AFTER_SHOOTOUT.getNumPoints() * nNumWinsOT;
    nPoints += GMatchResult.LOSS_AFTER_SHOOTOUT.getNumPoints() * nNumLossesOT;
    nPoints += GMatchResult.LOSS.getNumPoints() * nNumLosses;

    return nPoints;
  }

  /**
   * Returns the average amount of goals that should be initially awarded to a team
   * participating in a KO contest phase for each theoretically possible round.
   *
   * @param nNumMinMatchesPerRound the minimum number of matches to be played in one round
   * @param nNumMaxMatchesPerRound the maximum number of matches to be played in one round
   * @return the average number of goals to be awarded for one KO round
   */
  public static BigDecimal getInitialGoalsKORound( int nNumMinMatchesPerRound, int nNumMaxMatchesPerRound )
  {
    double dGoals = ( ( nNumMinMatchesPerRound + nNumMaxMatchesPerRound ) / 2 ) * GRulesConstants.GOALS_PER_MATCH_AVG;

    return BigDecimal.valueOf( dGoals ).setScale( 1, BigDecimal.ROUND_UP );
  }

  /**
   * <p>
   * Returns how many goals are initially added to a team's goals account at the beginning
   * of a round robin round competition where each team plays the given number of matches.
   * The goals given at index 0 are the average that shall be used if each team gets the
   * same amount of goals at the beginning.
   * </p>
   *
   * <p>
   * The number of attributed goals is calculated based upon the value given by
   * {@link #GOALS_PER_MATCH_AVG}.
   * </p>
   *
   * @param nNumTeams how many teams play in a round robin round
   * @param nNumRounds how often does each team play against each of the other teams
   * @param dTopDownPct the percentage (0.01 = 1%) of goals that the best team should get
   *          more and the worst less than the standard goals amount.
   * @return an array with at index 0 the average that shall be used if each team gets the
   *         same amount of goals at the beginning
   */
  public static BigDecimal[] getInitialGoalsRoundRobin( int nNumTeams, int nNumRounds, double dTopDownPct )
  {
    BigDecimal[] goals = new BigDecimal[nNumTeams + 1];
    int matchesPerTeam = ( nNumTeams - 1 ) * nNumRounds;

    double avgGoals = matchesPerTeam * GRulesConstants.GOALS_PER_MATCH_AVG;

    // best team gets bonus
    //
    double upperLimit = avgGoals * ( 1.0 + dTopDownPct );

    // worst team receives less goals than the avg.
    //
    double lowerLimit = avgGoals * ( 1.0 - dTopDownPct );

    // calculate step size
    //
    double step = ( upperLimit - lowerLimit ) / ( nNumTeams - 1 );

    // set the average number of goals
    //
    goals[0] = BigDecimal.valueOf( avgGoals ).setScale( 1, BigDecimal.ROUND_UP );

    // set the values for all ranks
    //
    double nextVal = upperLimit;
    for ( int i = 1; i < goals.length; i++ )
    {
      goals[i] = BigDecimal.valueOf( nextVal ).setScale( 1, BigDecimal.ROUND_UP );
      nextVal -= step;
    }

    return goals;
  }

  /**
   * Adds the correct CSS classes to the standings objects according to the number of
   * clubs marked as being promoted (category 1 and 2), as relegated or as midfielders.
   *
   * The passed list should be correctly ordered by rank (ascending). This method does not
   * order the list but assumes that this has already been done. If the list is not
   * ordered the applied rank types will not correspond to the actual ranks of the teams.
   *
   * @param listStandings the list of standings to colour (colouring means adding CSS
   *          classes)
   * @param element the contest element for which the standings should be updated
   * @throws IllegalArgumentException if the passed list is empty or <code>null</code> or
   *           has not the right number of elements
   */
  public static void applyRankTypes( List<GStandings> listStandings, GRoundRobinContestElement element )
  {
    int idx = 0;
    int nNumMidfield;
    boolean fFirstLeague = false;

    if ( listStandings == null || listStandings.isEmpty() )
      throw new IllegalArgumentException( "Passed standings list is null or empty." );

    if ( element.getContest().instanceOf( GLeague.class ) && element.getContest().getCategory() == 1 )
      fFirstLeague = true;

    // Check that the number of standings corresponds to the number of clubs
    //
    if ( listStandings.size() != element.getNumTeams() )
      throw new IllegalArgumentException( "The standings list has not the same number of elements as indicated by the"
          + "\nassociated round robin contest element with ID " + element.getId() + "\nThe list has "
          + listStandings.size() + " elements whereas there should be " + element.getNumTeams() + " elements." );

    nNumMidfield = element.getNumTeams() - element.getNumPromoted1() - element.getNumPromoted2()
        - element.getNumRelegated();

    if ( nNumMidfield < 0 )
      throw new IllegalStateException( "The number of teams marked as being promoted or relegated exceeds the number "
          + "\nof teams attached to the contest element with ID " + element.getId() );

    // Promoted 1
    //
    for ( int i = 0; i < element.getNumPromoted1(); i++ )
    {
      if ( fFirstLeague )
        listStandings.get( idx++ ).setRankType( GStandingsRankType.QUALIFIED_EU1 );
      else
        listStandings.get( idx++ ).setRankType( GStandingsRankType.PROMOTED_1 );
    }

    // Promoted 2
    //
    for ( int i = 0; i < element.getNumPromoted2(); i++ )
    {
      if ( fFirstLeague )
        listStandings.get( idx++ ).setRankType( GStandingsRankType.QUALIFIED_EU2 );
      else
        listStandings.get( idx++ ).setRankType( GStandingsRankType.PROMOTED_2 );
    }

    // Midfield
    //
    for ( int i = 0; i < nNumMidfield; i++ )
      listStandings.get( idx++ ).setRankType( GStandingsRankType.MIDFIELD );

    // Relegated
    //
    for ( int i = 0; i < element.getNumRelegated(); i++ )
      listStandings.get( idx++ ).setRankType( GStandingsRankType.RELEGATED );
  }

  /**
   * Creates a new contest round with a single {@link GKO3ContestElement} element. The
   * created contest round is not saved persistently. It's up to the caller to save it if
   * desired.
   *
   * @param parent the contest to which the round is attached
   * @param nNumRound the round number
   * @param roundType the round type
   * @param nBestLoserRank the best rank a team that does not qualify for the next round
   *          can obtain, pass 0 if it is the only contest round
   * @param nNumMatches the number of KO matches to be played in the round
   * @param fHopeRound indicates if this round is a hope round, allowing some promoted_2
   *          teams to rejoin the promoted_1 teams already qualified for the following
   *          round
   * @return the created GContestRound object
   */
  public static GContestRound createKORound( GContest parent, int nNumRound, GContestRoundType roundType,
      int nBestLoserRank, int nNumMatches, boolean fHopeRound )
  {
    GContestRound round = new GContestRound( parent, parent.getId() + "-" + nNumRound, nNumRound, roundType,
        GContestRoundFormat.KNOCK_OUT, fHopeRound );

    round.addContestElement( new GKO3ContestElement( round, round.getId() + "-1", "", nNumMatches ) );
    round.setBestPossibleLoserRank( nBestLoserRank );

    return round;
  }

  /**
   * Creates a new contest round with one or more {@link GRoundRobinContestElement}
   * elements. The created contest round is not saved persistently. It's up to the caller
   * to save it if desired. The created contest elements are labelled starting with 'A'.
   *
   * @param parent the contest to which the round is attached
   * @param nNumRound the round number
   * @param roundType the round type
   * @param nBestLoserRank the best rank a team that does not qualify for the next round
   *          can obtain, pass 0 if it is the only contest round
   * @param nNumGroups number of groups (contest elements) to be created inside of the
   *          round
   * @param nNumClubs the number of clubs per group
   * @param nNumRounds the number of rounds in each group
   * @param nNumPromoted1 the number of directly promoted teams (class 1)
   * @param nNumPromoted2 the number of indirectly promoted teams (class 2)
   * @param nNumRelegated the number of relegated teams
   * @param fHopeRound indicates if this round is a hope round, allowing some promoted_2
   *          teams to rejoin the promoted_1 teams already qualified for the following
   *          round
   * @return the created GContestRound object
   */
  public static GContestRound createRoundRobinRound( GContest parent, int nNumRound, GContestRoundType roundType,
      int nBestLoserRank, int nNumGroups, int nNumClubs, int nNumRounds, int nNumPromoted1, int nNumPromoted2,
      int nNumRelegated, boolean fHopeRound )
  {
    GContestRound round = new GContestRound( parent, parent.getId() + "-" + nNumRound, nNumRound, roundType,
        GContestRoundFormat.ROUND_ROBIN, fHopeRound );

    for ( int i = 1; i <= nNumGroups; i++ )
    {
      char cLabel = (char) ( 64 + i );
      GRoundRobinContestElement element = new GRoundRobinContestElement( round, getElementId( round.getId(), i, nNumGroups ), new String(
          new char[] { cLabel } ), nNumClubs, nNumRounds, nNumPromoted1, nNumPromoted2, nNumRelegated );

      round.addContestElement( element );
    }

    round.setBestPossibleLoserRank( nBestLoserRank );

    return round;
  }

  /**
   * Creates a new contest element ID. This method adds a leading 0 if the total number of
   * elements exceeds 9. As contest elements are often ordered by their ID, it is
   * important that the alphabetical order of IDs remains intact.
   *
   * @param strPrefix the ID prefix
   * @param nNumElement the number of the element for which to create an ID
   * @param nNumTotalElements the total number of elements
   * @return the contest element ID
   */
  private static String getElementId( String strPrefix, int nNumElement, int nNumTotalElements )
  {
    String strSuffix;

    if ( nNumTotalElements > 9 && nNumElement < 10 )
      strSuffix = "-0" + nNumElement;
    else
      strSuffix = "-" + nNumElement;

    return strPrefix + strSuffix;
  }
}
